﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using Lucene.Linq.Expressions;
using Lucene.Net.Documents;
using Lucene.Net.Search;

namespace Lucene.Linq.Mapping
{
    /// <summary>
    /// Enumerable hits reader that converts 
    /// </summary>
    /// <typeparam name="T">The return type from the enumerator</typeparam>
    internal class ProjectionReader<T> : IEnumerable<T>
    {
        private IEnumerator<T> _enumerator;


        internal ProjectionReader(Hits hits, int elementAt, Func<DocumentProjection, T> projector,
                                  IQueryProvider provider)
        {
            _enumerator = new SingleEnumerator(hits, elementAt, projector, provider);
        }

        internal ProjectionReader(Hits hits, int startIndex, int readCount, Func<DocumentProjection, T> projector,
                                  IQueryProvider provider)
        {
            _enumerator = new Enumerator(hits, startIndex, readCount, projector, provider);
        }

        #region IEnumerable<T> Members

        public IEnumerator<T> GetEnumerator()
        {
            IEnumerator<T> e = _enumerator;

            if (e == null)
            {
                throw new InvalidOperationException("Cannot enumerate more than once");
            }

            _enumerator = null;
            return e;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region Nested type: BaseProjection

        private abstract class BaseProjection : DocumentProjection
        {
            protected readonly Hits _hits;
            protected readonly Func<DocumentProjection, T> _projector;
            private readonly IQueryProvider _provider;

            protected BaseProjection(Hits hits, Func<DocumentProjection, T> projector, IQueryProvider provider)
            {
                _hits = hits;
                _projector = projector;
                _provider = provider;
            }

            protected static object GetValue(Document document, string fieldName, Type fieldType)
            {
                string value;
                Field field = document.GetField(fieldName);

                if (field != null)
                    value = field.StringValue();
                else
                    return null;

                if (fieldType == typeof(string))
                    return value;

                TypeConverter converter = TypeDescriptor.GetConverter(fieldType);
                return converter.CanConvertFrom(typeof(string)) ? converter.ConvertFrom(value) : null;
            }

            private static bool CanEvaluateLocally(Expression expression)
            {
                if (expression.NodeType == ExpressionType.Parameter || expression.NodeType.IsIndexExpression())
                {
                    return false;
                }

                return true;
            }

            public override IEnumerable<TItem> ExecuteSubQuery<TItem>(LambdaExpression query)
            {
                var projection =
                    (ProjectionExpression)
                    new Replacer().Replace(query.Body, query.Parameters[0], Expression.Constant(this));
                projection = (ProjectionExpression)Evaluator.PartialEval(projection, CanEvaluateLocally);
                var result = (IEnumerable<TItem>)_provider.Execute(projection);
                var list = new List<TItem>(result);

                if (typeof(IQueryable<TItem>).IsAssignableFrom(query.Body.Type))
                {
                    return list.AsQueryable();
                }

                return list;
            }
        }

        #endregion

        #region Nested type: Enumerator

        private class Enumerator : BaseProjection, IEnumerator<T>
        {
            private readonly bool isIHit;
            private readonly int readCount;
            private readonly bool useReadCount;
            private int count;
            private T current;
            private Document currentDocument;
            private int index;

            internal Enumerator(Hits hits,
                                int startIndex,
                                int readCount,
                                Func<DocumentProjection, T> projector,
                                IQueryProvider provider)
                : base(hits, projector, provider)
            {
                index = startIndex;
                this.readCount = readCount;

                // use the read count if it's more than 0
                useReadCount = readCount > 0;
                isIHit = typeof(IHit).IsAssignableFrom(typeof(T));
            }

            #region IEnumerator<T> Members

            public T Current
            {
                get { return current; }
            }

            object IEnumerator.Current
            {
                get { return current; }
            }

            public bool MoveNext()
            {
                if ((useReadCount == false) || (useReadCount && (count < readCount)))
                {
                    if (index < _hits.Length())
                    {
                        currentDocument = _hits.Doc(index);
                        current = _projector(this);

                        // Add the IHit properties
                        if (isIHit)
                        {
                            var hit = (current as IHit);
                            hit.Relevance = _hits.Score(index);
                            hit.DocumentId = _hits.Id(index);
                        }

                        index++;
                        count++;
                        return true;
                    }
                }

                return false;
            }

            public void Reset()
            {
                count = 0;
            }

            public void Dispose()
            {
            }

            #endregion

            public override object GetValue(string fieldName, Type fieldType)
            {
                return GetValue(currentDocument, fieldName, fieldType);
            }
        }

        #endregion

        #region Nested type: SingleEnumerator

        private class SingleEnumerator : BaseProjection, IEnumerator<T>
        {
            private readonly T current;
            private readonly Document document;
            private readonly int documentIndex;

            private bool isAtEnd;

            internal SingleEnumerator(Hits hits,
                                      int documentIndex,
                                      Func<DocumentProjection, T> projector,
                                      IQueryProvider provider)
                : base(
                    hits,
                    projector,
                    provider)
            {
                this.documentIndex = documentIndex;
                document = hits.Doc(documentIndex);

                if (document != null)
                {
                    current = projector(this);
                }

                isAtEnd = false;
            }

            #region IEnumerator<T> Members

            public void Dispose()
            {
            }

            public bool MoveNext()
            {
                if (isAtEnd)
                {
                    return true;
                }
                else
                {
                    isAtEnd = true;
                    return false;
                }
            }

            public void Reset()
            {
                isAtEnd = false;
            }

            public T Current
            {
                get { return current; }
            }

            object IEnumerator.Current
            {
                get { return Current; }
            }

            #endregion

            public override object GetValue(string fieldName, Type fieldType)
            {
                return GetValue(document, fieldName, fieldType);
            }
        }

        #endregion
    }
}